Skip to content

[SPARK-58723][SQL] Support the ANSI SQL/JSON_QUERY function - #57957

Open
ganeshashree wants to merge 3 commits into
apache:masterfrom
ganeshashree:SPARK-58723
Open

[SPARK-58723][SQL] Support the ANSI SQL/JSON_QUERY function#57957
ganeshashree wants to merge 3 commits into
apache:masterfrom
ganeshashree:SPARK-58723

Conversation

@ganeshashree

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Add the ANSI SQL:2016 JSON_QUERY function (feature T828), which extracts the JSON value located by a SQL/JSON path from a JSON input and returns it as JSON text.

JSON_QUERY(jsonExpr, path
           [ RETURNING type ]
           [ (WITHOUT | WITH [CONDITIONAL | UNCONDITIONAL]) [ARRAY] WRAPPER ]
           [ (KEEP | OMIT) QUOTES ]
           [ (NULL | ERROR | EMPTY ARRAY | EMPTY OBJECT) ON EMPTY ]
           [ (NULL | ERROR | EMPTY ARRAY | EMPTY OBJECT) ON ERROR ])

It is implemented as a parser-only production (no FunctionRegistry entry), mirroring the merged JSON_VALUE (SPARK-58685) and reusing the JSON path evaluator from JSON_TABLE (SPARK-58366). The matched object/array/scalar is serialized verbatim; a scalar is emitted as JSON text under the default WITHOUT ARRAY WRAPPER, OMIT QUOTES unquotes a scalar string, the array wrapper wraps per the standard (CONDITIONAL wraps only a scalar), and ON EMPTY / ON ERROR default to NULL. RETURNING is STRING-only for now (VARIANT deferred); paths are wildcard-free.

Example (j = {"id":7,"name":"Ada","tags":["x","y"],"addr":{"city":"NYC"}}):

SELECT json_query(j, '$.addr');                        -- {"city":"NYC"}
SELECT json_query(j, '$.tags');                        -- ["x","y"]
SELECT json_query(j, '$.id');                          -- 7        (scalar as JSON text)
SELECT json_query(j, '$.name' OMIT QUOTES);            -- Ada
SELECT json_query(j, '$.tags[0]' WITH ARRAY WRAPPER);  -- ["x"]
SELECT json_query(j, '$.missing' EMPTY ARRAY ON EMPTY);-- []
SELECT json_query('not json', '$.a');                  -- NULL     (NULL ON ERROR default)

Follow-ups (shared with JSON_VALUE, out of scope here): default-collation rewrite of the result, whole-stage codegen, and cross-expression parse sharing for sibling projections.

Why are the changes needed?

JSON_QUERY is the SQL-standard way to extract an object/array/scalar fragment from JSON, supported by Oracle, SQL Server, PostgreSQL, Trino, Flink, and others. Spark had no equivalent, forcing migrated queries onto get_json_object, whose NULL/error semantics differ (it conflates "missing" and "parse error", always returns STRING, and offers no wrapper/quotes control). This is the natural follow-on to JSON_VALUE.

Does this PR introduce any user-facing change?

Yes. It adds the new JSON_QUERY SQL function and its documentation, and adds JSON_QUERY, WRAPPER, QUOTES, KEEP, OMIT, CONDITIONAL, UNCONDITIONAL, and OBJECT as non-reserved keywords (usable as identifiers in both modes). No change to existing behavior.

How was this patch tested?

  • New JsonQuerySuite (end-to-end): object/array/scalar extraction, JSON null, all wrapper modes, KEEP/OMIT QUOTES (incl. escapes), EMPTY ARRAY/OBJECT, ON EMPTY/ON ERROR, malformed/trailing input, RETURNING VARCHAR/CHAR normalization, direct-construction validation, and keyword-as-identifier.
  • Golden files in json-functions.sql; regenerated keywords*.sql.out.
  • ExpressionParserSuite for every clause spelling; SQLKeywordSuite; SparkThrowableSuite; and TableIdentifierParserSuite / JsonExpressionsSuite for regression.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Claude Opus 4.8)

Add the ANSI SQL:2016 JSON_QUERY function (feature T828): extract the JSON
value located by a SQL/JSON path from a JSON input and return it as JSON text.

Syntax:

  JSON_QUERY(jsonExpr, path
             [ RETURNING type ]
             [ (WITHOUT | WITH [CONDITIONAL | UNCONDITIONAL]) [ARRAY] WRAPPER ]
             [ (KEEP | OMIT) QUOTES ]
             [ (NULL | ERROR | EMPTY ARRAY | EMPTY OBJECT) ON EMPTY ]
             [ (NULL | ERROR | EMPTY ARRAY | EMPTY OBJECT) ON ERROR ])

Implemented as a parser-only production (no FunctionRegistry entry), mirroring
the merged JSON_VALUE (SPARK-58685) and reusing the JSON path evaluator from
JSON_TABLE. The matched object/array/scalar is serialized verbatim; a scalar is
emitted as JSON text under the default WITHOUT ARRAY WRAPPER, OMIT QUOTES
unquotes a scalar string, the array wrapper wraps per the standard, and
ON EMPTY / ON ERROR default to NULL. RETURNING is STRING-only for now (VARIANT
deferred) and CHAR/VARCHAR are normalized to STRING. Paths are wildcard-free.
@uros-b

uros-b commented Aug 12, 2026

Copy link
Copy Markdown
Member

Thank you @ganeshashree!

…_QUERY

Update the JSON_VALUE reference page: now that JSON_QUERY exists, object/array
fragment extraction should point to JSON_QUERY (and JSON_TABLE for producing
rows), instead of only JSON_TABLE / get_json_object.

Co-authored-by: Isaac

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 blocking, 1 non-blocking, 0 nits.
The feature is coherent and well covered; one avoidable per-row parsing cost should be considered before merge.

Suggestions (1)

  • Non-blocking: sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala:1003: Avoid reparsing each OMIT QUOTES result by carrying the decoded string from the existing parser. -- see inline

Verification

I traced JSON_QUERY from its grammar alternative through AstBuilder, Catalyst type checking, JsonTableEvaluator.queryLookup, and the wrapper/quote result branches. The SQL golden outputs and dedicated suite cover the documented defaults, malformed and missing inputs, wrapper modes, quote handling, result type, and structured errors.

Address review feedback: the OMIT QUOTES path re-parsed each matched
scalar string after queryLookup had already parsed and serialized it,
adding a second Jackson parse per input row. Carry the decoded
VALUE_STRING content out of the existing parser in JsonQueryLookup.Found
(as `unquoted`, equal to the raw text for every non-string value, for
which OMIT QUOTES is a no-op) and use it directly, so no reparse occurs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants